package io.gitee.thant.utils;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Base64;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Pattern;

public class FileUtil extends org.apache.commons.io.FileUtils {
	private static final FileUtil _only = new FileUtil();
	
	private FileUtil() {}
	
	public static String getMimeType(byte[] imgDat) {
		if ((int)(imgDat[0]&0x0FF)==0xFF && (int)(imgDat[1]&0x0FF)==0xD8 && (int)(imgDat[2]&0x0FF)==0xFF) {
			return "image/jpeg";
		} else if ((int)(imgDat[0]&0x0FF)==0x47 && (int)(imgDat[1]&0x0FF)==0x49 && (int)(imgDat[2]&0x0FF)==0x46 && (int)(imgDat[3]&0x0FF)==0x38) {
			return "image/gif";
		} else if ((int)(imgDat[0]&0x0FF)==0x89 && (int)(imgDat[1]&0x0FF)==0x50 && (int)(imgDat[2]&0x0FF)==0x4E && (int)(imgDat[3]&0x0FF)==0x47) {
			return "image/png";
		} else if ((int)(imgDat[0]&0x0FF)==0x52 && (int)(imgDat[1]&0x0FF)==0x49 && (int)(imgDat[2]&0x0FF)==0x46 && (int)(imgDat[3]&0x0FF)==0x46) {
			return "image/webp";
		} else if ((int)(imgDat[0]&0x0FF)==0x49 && (int)(imgDat[1]&0x0FF)==0x44 && (int)(imgDat[2]&0x0FF)==0x33) {
			return "audio/mpeg";
		} else {
			return "*/*";
		}
	}
	
	public static String wrapHTMLBase64(byte[] rawdata) {
		StringBuilder sb = new StringBuilder();
		sb.append("data:").append(FileUtil.getMimeType(rawdata)).append(";base64,")
			.append(new String(Base64.getEncoder().encode(rawdata))).toString();
		return sb.toString();
	}

	/* 这个版本不支持encode和append参数 而且用File做key有问题
	private static Thread logWriter = null;
	private final static Map<File, StringBuilder> buffer = new LinkedHashMap<File, StringBuilder>();
	public static void ansyncWriteStringToFile(File file, String data, String encode, boolean append) {
		synchronized(buffer) {
			
			StringBuilder sb = buffer.get(file);
			if (null == sb) {
				sb = new StringBuilder(data);
				buffer.put(file, sb);
			} else if (append) {
				sb.append(data);
			} else {
				sb.setLength(0);
				sb.append(data);
			}
			
			if (null == logWriter || !logWriter.isAlive()) {
				System.out.println("fileWriter thread created.");
				
				logWriter = new Thread("logWriter") {
					@Override
					public void run() {
						int cnt = 0;
						for (;;) {
							File fp = null;
							String content = null;
							
							int times;
							for (times=0; times<1000; ++times) {
								synchronized(buffer) {
									
									Iterator<Entry<File,StringBuilder>> it = buffer.entrySet().iterator();
									if (null != it && it.hasNext()) {
										Entry<File, StringBuilder> item = it.next();
										fp = item.getKey();
										content = item.getValue().toString();
										it.remove();
										break;
									}
									
								}

								try { Thread.sleep(10); } catch (Exception e) { }
								continue;
							}
							if (null == fp) {
								logWriter = null;
								break;
							}
							
							try {
								writeStringToFile(fp, content, encode, append);
								cnt++;
							} catch (Exception e) {
								e.printStackTrace();
							}
							
						}
						
						System.out.println("fileWriter thread dead. deal="+cnt);
					}
				};
				logWriter.start();
			}
		}
	}*/
	
	//这个版本支持encode和append参数，修复了多个bug 而且内存消耗进一步降低
	private static Thread logWriter = null;
	private final static Map<String, ByteBuilder> buffer = new LinkedHashMap<String, ByteBuilder>();
	public static void ansyncWriteStringToFile(File file, byte[] data, boolean append) {
		String path = null;
		try {
			path = file.getCanonicalPath();
		} catch (Exception e) {	}
		if (null == path) return;
		
		synchronized(buffer) {
			ByteBuilder bb = buffer.get(path);
			if (null == bb) {
				bb = new ByteBuilder().append((byte)(append ? 1 : 0));//首字节1表示append 0表示覆盖
				buffer.put(path, bb);
			}
			if (!append) {
				bb.setLength(0).append((byte)0);
			}
			bb.append(data);
			/*if (bb.size()>10240) {
				buffer.put(path+"*"+StringUtil.getUUID(), bb);
				buffer.remove(path);
			}*/
		}
			if (null == logWriter) {
				System.out.println("fileWriter thread created.");
				
				logWriter = new Thread("logWriter") {
					@Override
					public void run() {
						int cnt = 0;
						for (;;) {
							File fp = null;
							boolean append = false;
							byte[] content = null;
							
							for (int times=0; times<1000; ++times) {
								synchronized(buffer) {
									
									Iterator<Entry<String, ByteBuilder>> it = buffer.entrySet().iterator();
									if (null != it && it.hasNext()) {
										Entry<String, ByteBuilder> item = it.next();
										fp = new File(item.getKey());
										content = item.getValue().toArrayRef();
										append = (1 == content[0]);
										it.remove();
										break; //退出等待
									}
									
								}

								try { Thread.sleep(10); } catch (Exception e) { }
							}
							if (null == fp || null == content) {
								break; //退出线程
							}
							
							try {
								//System.out.println(content.length-1);
								writeByteArrayToFile(fp, content, 1, content.length-1, append);
								cnt++;
							} catch (Exception e) {
								e.printStackTrace();
							}
						}
						
						logWriter = null;
						System.out.println("fileWriter thread dead. deal="+cnt);
					}
				};
				logWriter.start();
			}
		//}
	}

	public static void ansyncWriteStringToFile(File file, String data, String encode, boolean append) {
		byte[] ba = null;
		try {
			ba = data.getBytes(encode);
		} catch (Exception e) { }
		if (null == ba) return;
		
		ansyncWriteStringToFile(file, ba, append);
	}

	public static String hashOfUUID() {
		return hashOfID(StringUtil.getUUID());
	}

	public static String hashOfID(String id) {
		StringBuilder sb = new StringBuilder();
		if (32 == id.length()) {
			return sb.append(id.substring(6,8)).append(File.separatorChar)
				.append(id.substring(10,12)).append(File.separatorChar)
				.append(id.substring(14,16)).append(File.separatorChar)
				.append(id.substring(18,20)).append(File.separatorChar)
				.append(id.substring(30))
				.toString();
		} else {
			for (int i=0; i<id.length(); i+=2) {
				sb.append(File.separatorChar).append(i+2>id.length() ? id.substring(i) : id.substring(i, i+2));
			}
			return sb.length()>0 ? sb.substring(1) : id;
		}
	}
	
	public static String buildPath(String... parts) {
		StringBuilder sb = new StringBuilder();
		for (int i=0; i<parts.length; ++i) {
			sb.append(File.separatorChar).append(parts[i]);
		}
		String path = sb.toString();
		return path.startsWith(File.separator) ? path.substring(1) : path;
	}
	
	/*
	 * 文件监控API
	 */
	private static WatchService wsrv = null; //监控服务
	private static Thread _watcher = null;   //监控线程
	private final static Map<Path, Object[]> watchDef = new HashMap<Path, Object[]>(); //监控参数
	//监控项
	public class WatchItem {
		public final String path;
		public final String name;
		public final String type;
		WatchItem(String path, String name, String type) {
			this.path = path;
			this.name = name;
			this.type = type;
		}
	}
	
	/*
	 * 监控目录
	 * 参数:
	 *   String pathstr : 目录
	 *   String filter  : 文件名过滤规则 正则 null表示不过滤
	 *   Consumer<WatchItem> callback 回调函数
	 */
	public static void watchPath(String pathstr, String filter, Consumer<WatchItem> callback) {
		Path path = Paths.get(pathstr).toAbsolutePath();
		if (null == wsrv) {
			try {
				wsrv = FileSystems.getDefault().newWatchService();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		synchronized(wsrv) {
			Pattern pattern = StringUtil.isEmpty(filter) ? null : Pattern.compile(filter, Pattern.CASE_INSENSITIVE);
			boolean exists = watchDef.containsKey(pathstr);
			Object[] dat = new Object[]{pattern, callback};
			watchDef.put(path, dat);
			if (!exists) {
				try {
					path.register(wsrv, new WatchEvent.Kind[]{
						StandardWatchEventKinds.ENTRY_MODIFY,
						StandardWatchEventKinds.ENTRY_CREATE,
						StandardWatchEventKinds.ENTRY_DELETE});
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
		
		if (null == _watcher || !_watcher.isAlive()) {
			_watcher = new Thread("_watcher") {
				@Override
				public void run() {
					int waitlen = 1;
					boolean loop = true;
					while (loop) {
						WatchKey key = null;
						synchronized(wsrv) {
							try {
								key = wsrv.poll(waitlen, TimeUnit.MILLISECONDS);
							} catch (InterruptedException e) {
								loop = false;
								continue;
							}
						}
						if (key == null) {
							if (waitlen<100) waitlen++;
							continue;
						}
						waitlen = 1;

						WatchEvent<?>[] array = null;
						Path dir = (Path)key.watchable();
						array = key.pollEvents().toArray(new WatchEvent<?>[0]);
						key.reset();
						
						for (WatchEvent<?> event : array) {
							Path p = (Path)event.context();
							Object[] dat = watchDef.get(dir);
							Pattern pn = (Pattern)dat[0];
							@SuppressWarnings("unchecked")
							Consumer<WatchItem> call = (Consumer<WatchItem>)dat[1];
							if ((null == pn || pn.matcher(p.toString()).find()) && null != call) {
								try {
									WatchItem obj = _only.new WatchItem(
										dir.toAbsolutePath().toString(),
										p.toString(), //p是相对路径 而dir.resolve(p)是绝对路径
										event.kind().name());
									call.accept(obj);
								} catch (Exception e) {}
							}
						}
					}
					_watcher = null;
				}
			};
			_watcher.start();
		}
	}
	
	private static Consumer<WatchItem> handle = null; //变更日志记录者匿名函数
	private static BiFunction<String, String, Boolean> callbackhandle = null; //变更日志记录者回调，这个回调是全局的
	private static ConcurrentHashMap<String, Object> _watchLoggerMap = null; //变更日志记录，key为文件路径，value是变更类型(HashSet)
	/*
	 * 变更日志记录者
	 * 参数
	 *   BiFunction<String, String, Boolean> custom : 定制回调(变更文件路径,变更类型,是否记录变更)
	 *     也即全局的callbackhandle，该参数可为null，为null时不做任何处理，非null的话会替换上次调用的参数
	 */
	public static Consumer<WatchItem> watchLogger(BiFunction<String, String, Boolean> custom) {
		if (null != custom) {
			callbackhandle = custom;
		}
		if (null == handle) {
			handle = new Consumer<WatchItem>() {
				@Override
				@SuppressWarnings("unchecked")
				public void accept(WatchItem watch) {
					if (null == _watchLoggerMap) {
						_watchLoggerMap = new ConcurrentHashMap<String, Object>();
					}
					String key = (watch.path+File.separatorChar+watch.name).toLowerCase();
					String type = watch.type;
					_watchLoggerMap.compute(key, (k, v)->{
						if (null == v) {
							v = new HashSet<String>();
						}
						((HashSet<String>)v).add(type);
						return v;
					});
					if (callbackhandle != null && !callbackhandle.apply(key, type)) {
						_watchLoggerMap.remove(key);
					}
				}
			};
		}
		return handle;
	}

	/*
	 * 查询文件变更
	 * 参数
	 *   String path: 查询文件的绝对路径,不区分大小写
	 *   String type: 文件变更类型，null表示任何类型都可以(ENTRY_MODIFY|ENTRY_DELETE|ENTRY_CREATE)
	 *   boolean remove: 查询后是否删除记录
	 * 返回值 true/false 是否存在指定的文件变更
	 */
	public static boolean queryFileChange(String path, String type, boolean remove) {
		if (null == _watchLoggerMap || null == path) return false;
		
		path = path.toLowerCase();
		@SuppressWarnings("unchecked")
		Set<String> set = (Set<String>)(remove ?
			_watchLoggerMap.remove(path) :
			_watchLoggerMap.get(path));
		if (null == set) return false;
		
		return null == type || set.contains(type);
	}

	/*
	 * 获取文件变更记录迭代器，返回值可能为null
	 */
	public static Iterator<Entry<String, Object>> getFileChangeIterator() {
		if (null == _watchLoggerMap) return null;
		return _watchLoggerMap.entrySet().iterator();
	}
	
	public static Thread ansyncFileList(File curpath, FileFilter ff, Consumer<File> func, int priority) {
		if (null == func) return null;
		if (priority<Thread.MIN_PRIORITY || priority>Thread.MAX_PRIORITY) {
			priority=Thread.MIN_PRIORITY;
		}
		
		Thread thd = new Thread("fileLister"+System.currentTimeMillis()) {
			@Override
			public void run() {
				fileList(curpath, ff, func, true);
			}
		};
		thd.setPriority(priority);
		thd.start();
		return thd;
	}
	
	public static void fileList(File curpath, FileFilter ff, Consumer<File> func, boolean yeild) {
		if (!curpath.exists()) return;
		if (curpath.isFile()) {
			func.accept(curpath);
			if (yeild) {
				Thread.yield();
			}
		}
		if (curpath.isDirectory()) {
			File chdA[] = curpath.listFiles(ff);
			for (int i=0; i<chdA.length; ++i) {
				fileList(chdA[i], ff, func, yeild);
			}
		}
	}
}
